// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';

import 'package:analysis_server/src/socket_server.dart';
import 'package:analysis_server/src/status/diagnostics.dart';

/// Instances of the class [AbstractHttpHandler] handle HTTP requests.
abstract class AbstractHttpHandler {
  /// Handle a GET request received by the HTTP server.
  void handleGetRequest(HttpRequest request);

  /// Handle a POST request received by the HTTP server.
  void handlePostRequest(HttpRequest request);

  /// Handle a request to upgrade to a WebSocket.
  void handleWebSocketRequest(HttpRequest request);
}

/// Instances of the class [HttpServer] implement a simple HTTP server. The
/// server:
///
/// - listens for an UPGRADE request in order to start an analysis server
/// - serves diagnostic information as html pages
class HttpAnalysisServer {
  /// Number of lines of print output to capture.
  static const int MAX_PRINT_BUFFER_LENGTH = 1000;

  /// An object that can handle either a WebSocket connection or a connection
  /// to the client over stdio.
  final AbstractSocketServer _socketServer;

  /// An object that can handle HTTP requests.
  late final AbstractHttpHandler _httpHandler = DiagnosticsSite(
    _socketServer,
    _printBuffer,
  );

  /// Future that is completed with the HTTP server once it is running.
  Future<HttpServer>? _serverFuture;

  /// Last PRINT_BUFFER_LENGTH lines printed.
  final List<String> _printBuffer = <String>[];

  /// Initialize a newly created HTTP server.
  HttpAnalysisServer(this._socketServer);

  /// Return the port this server is bound to.
  Future<int?> get boundPort async {
    return (await _serverFuture)?.port;
  }

  void close() {
    _serverFuture?.then((HttpServer server) {
      server.close();
    });
  }

  /// Record that the given line was printed out by the analysis server.
  void recordPrint(String line) {
    _printBuffer.add(line);
    if (_printBuffer.length > MAX_PRINT_BUFFER_LENGTH) {
      _printBuffer.removeRange(
        0,
        _printBuffer.length - MAX_PRINT_BUFFER_LENGTH,
      );
    }
  }

  /// Begin serving HTTP requests over the given port.
  Future<int?> serveHttp([int? initialPort]) async {
    if (_serverFuture != null) {
      return boundPort;
    }

    try {
      _serverFuture = HttpServer.bind(
        InternetAddress.loopbackIPv4,
        initialPort ?? 0,
      );

      var server = (await _serverFuture)!;
      _handleServer(server);
      return server.port;
    } catch (ignore) {
      // If we can't bind to the specified port, don't remember the broken
      // server.
      _serverFuture = null;

      return null;
    }
  }

  /// Attach a listener to a newly created HTTP server.
  void _handleServer(HttpServer httpServer) {
    httpServer.listen((HttpRequest request) async {
      if (WebSocketTransformer.isUpgradeRequest(request) &&
          // For WebSockets, verify we're same origin (since the browser would
          // not).
          request.headers.value('origin') == request.requestedUri.origin) {
        _httpHandler.handleWebSocketRequest(request);
      } else if (request.method == 'GET') {
        _httpHandler.handleGetRequest(request);
      } else if (request.method == 'POST') {
        _httpHandler.handlePostRequest(request);
      } else {
        _returnUnknownRequest(request);
      }
    });
  }

  /// Return an error in response to an unrecognized request received by the
  /// HTTP server.
  void _returnUnknownRequest(HttpRequest request) {
    var response = request.response;
    response.statusCode = HttpStatus.notFound;
    response.headers.contentType = ContentType.text;
    response.write('Not found');
    response.close();
  }
}
